1 module hip.util.path;
2 import hip.util.string;
3 import hip.util.system;
4 //Node required for buildFolderTree
5 public import hip.util.data_structures: Node;
6 
7 version(Windows) 
8     enum defaultCaseSensitivity = false;
9 else version(Darwin) 
10     enum defaultCaseSensitivity = false;
11 else// version(Posix) 
12     enum defaultCaseSensitivity = true;
13 
14 version(Windows)
15 {
16     enum pathSeparator = '\\';
17     enum otherSeparator = '/';
18 }
19 else
20 {
21     enum pathSeparator = '/';
22     enum otherSeparator = '\\';
23 }
24 
25 string[] pathSplitter(string path) @safe pure nothrow
26 {
27     string[] ret;
28     foreach(p; pathSplitterRange(path))
29         ret~= p;
30     return ret;
31 }
32 
33 auto pathSplitterRange(string path) pure @safe nothrow @nogc
34 {
35     struct PathRange
36     {
37         string path;
38         size_t indexRight = 0;
39 
40         bool empty() @safe pure nothrow @nogc {return indexRight >= path.length;}
41         string front() @safe pure nothrow @nogc
42         {
43             size_t i = indexRight;
44             while(i < path.length && path[i] != '\\' && path[i] != '/')
45                 i++;
46             indexRight = i;
47             return path[0..indexRight];
48         }
49         void popFront() @safe pure nothrow @nogc
50         {
51             if(indexRight+1 < path.length)
52             {
53                 path = path[indexRight+1..$];
54                 indexRight = 0;
55             }
56             else
57                 indexRight+= 1; //Guarantees empty
58         }
59     }
60 
61     return PathRange(path);
62 }
63 
64 bool isRootOf(string theRoot, string ofWhat) pure nothrow @nogc
65 {
66     auto pathA = pathSplitterRange(theRoot);
67     auto pathB = pathSplitterRange(ofWhat);
68 
69     for(; !pathA.empty && !pathB.empty; pathA.popFront, pathB.popFront)
70     {
71         string compA = pathA.front;
72         string compB = pathB.front;
73         if(compA != compB)
74             return false;
75     }
76     return true;
77 }
78 
79 
80 
81 string relativePath(bool caseSensitive = defaultCaseSensitivity)(string filePath, string base) pure nothrow @safe
82 {
83     int commonIndex = 0;
84     bool isEqual = true;
85     for(int i = 0; i < base.length; i++)
86     {
87         if(i == filePath.length || (caseSensitive ? base[i] != filePath[i] : base[i].toLowerCase != filePath[i].toLowerCase))
88         {
89             isEqual = false;
90             break;
91         }
92         else if(base[i] == pathSeparator)
93             commonIndex = cast(int)i;
94     }
95     if(isEqual)
96     {
97         if(filePath.length == base.length)
98             return ".";
99         else //If the base string is a subset, return part after base.
100             return filePath[base.length + (filePath[base.length] == pathSeparator ? 1 : 0)..$];
101     } 
102     else if(commonIndex == 0)
103         return filePath;
104 
105     string ret;
106     for(uint i = commonIndex; i < base.length; i++)
107     {
108         if(base[i] == pathSeparator)
109             ret~= ".."~pathSeparator;
110     }
111     ret~= filePath[commonIndex] == pathSeparator ? filePath[commonIndex+1..$] : filePath[commonIndex..$];
112     return ret;
113 }
114 
115 bool isAbsolutePath(string fPath) pure nothrow @nogc @safe
116 {
117     if(fPath == null)
118         return false;
119     version(Posix)
120         if(fPath[0] != '/')
121             return false;
122     version(Windows)
123     {
124         if(fPath.length < 3)
125             return false;
126         if(!(fPath[0].isUpperCase && fPath[1] == ':' && fPath[2] == '\\'))
127             return false;
128     }
129     for(size_t i = 0; i < fPath.length; i++)
130         if(i + 2 < fPath.length && fPath[i] == '.' && fPath[i+1] == '.' && fPath[i+2] == pathSeparator)
131             return false;
132     return true;
133 }
134 
135 
136 
137 char determineSeparator (string filePath) pure nothrow @nogc @safe
138 {
139     size_t i = 0;
140     while(i < filePath.length && filePath[i] != '/' && filePath[i] != '\\')
141         i++;
142     return i < filePath.length ? filePath[i] : '\0';
143 }
144 
145 ///Will get the directory name until a trailing separator or return 
146 string dirName(string filePath) pure nothrow @nogc @safe
147 {
148     char sep = determineSeparator(filePath);
149     if(sep == '\0')
150         return filePath;
151     int last = filePath.lastIndexOf(sep);
152     if(last == -1)
153         return filePath;
154     return filePath[0..last];
155 }
156 
157 
158 string filename(string filePath) @safe pure nothrow @nogc
159 {
160     char sep = determineSeparator(filePath);
161     if(sep == '\0')
162         return filePath;
163     int last = filePath.lastIndexOf(sep);
164     if(last == -1)
165         return filePath;
166     return filePath[last+1..$];
167 }
168 
169 alias baseName = filename;
170 
171 ref string filename(return ref string filePath, string newFileName) @safe pure nothrow
172 {
173     return filePath = replaceFileName(filePath, newFileName);
174 }
175 
176 string filenameNoExt(string filePath) @safe pure nothrow @nogc
177 {
178     string f = filePath.filename;
179     if(f == "")
180         return "";
181     int last = f.lastIndexOf(".");
182     if(last == -1)
183         return f;
184     return f[0..last];
185 }
186 
187 string replaceFileName(string filePath, string newFileName) @safe pure nothrow
188 {
189     char sep = determineSeparator(filePath);
190     string[] p = pathSplitter(filePath);
191     p[$-1] = newFileName;
192     return ((p[0] == "" && sep == '/') ? "/" : "") ~ joinPath(sep, p);
193 }
194 
195 string normalizePath(string path)
196 {
197     string[] normalized;
198     foreach(p; pathSplitterRange(path))
199     {
200         if(p == ".")
201             continue;
202         else if(p == "..")
203         {
204             if(normalized.length > 0)
205                 normalized = normalized[0..$-1];
206             else
207                 normalized~= p;
208         }
209         else
210             normalized~= p;
211 
212     }
213     return normalized.joinPath;
214 }
215 
216 
217 
218 /**
219 *   Extension getter
220 ```d
221 string myFile = "test.png";
222 writeln(myFile.extension); //png
223 ```
224 */
225 string extension(string pathOrFilename) pure nothrow @nogc @safe
226 {
227     auto ind = pathOrFilename.lastIndexOf(".");
228     if(ind == -1)
229         return "";
230     return pathOrFilename[cast(uint)ind+1..$];
231 }
232 
233 /**
234 *   Extension setter.
235 *   Usage:
236 ```d
237    string test = "test.png"
238    test.extension = "txt";
239    writeln(test); //test.txt
240 ```
241 */
242 ref string extension(return ref string pathOrFilename, string newExt)
243 {
244     auto ind = pathOrFilename.lastIndexOf(".");    
245     if(ind != -1 && ind != pathOrFilename.length)
246     {
247         if(newExt.length == 0)
248             pathOrFilename = pathOrFilename[0..ind];
249         else if(newExt[0] != '.')
250             pathOrFilename = pathOrFilename[0..ind+1]~newExt;
251         else
252             pathOrFilename = pathOrFilename[0..ind+1]~newExt[1..$];
253     }
254     return pathOrFilename;
255 }
256 
257 string extension(string pathOrFilename, string newExt)
258 {
259     pathOrFilename = pathOrFilename.extension(newExt);
260     return pathOrFilename;
261 }
262 
263 string joinPath(char separator, in string[] paths ...) @safe pure nothrow
264 {
265     if(paths.length == 1)
266         return paths[0];
267     string output;
268     for(int i = 0; i < paths.length; i++)
269     {
270         string filePath = paths[i];
271         string next = i+1 < paths.length ? paths[i+1] : "";
272 
273         if(filePath == "")
274             continue;
275         
276         output~=paths[i];
277         if(next != "" && next[0] != separator  &&
278         paths[i][$-1] != separator)
279             output~=separator;
280     }
281     return output;
282 }
283 
284 string joinPath(in string[] paths ...) @safe pure nothrow
285 {
286     char sep;
287     foreach(p; paths)
288     {
289         sep = determineSeparator(p);
290         if(sep != '\0')
291             break;
292     }
293     if(sep == '\0')
294         sep = pathSeparator;
295     return joinPath(sep, paths);
296 }
297 
298 
299 public Node!string buildFolderTree(string[] filesList)
300 {
301     alias DirNode = Node!string;
302     DirNode root = new DirNode(filesList[0]);
303 
304     scope DirNode[] dirStack = [root];
305     
306     for(size_t i = 1; i < filesList.length; i++)
307     {
308         int currStack = 0;
309         foreach(pathPart; pathSplitterRange(filesList[i]))
310         {
311             if(pathPart.extension != "") //It is a leaf if it has an extension
312             {
313                 dirStack[$-1].addChild(pathPart);
314             }
315             else if(currStack >= dirStack.length) //If we have more parts than the stack has children, add to the stack
316             {
317                 //Add child to the last
318                 dirStack~= dirStack[$-1].addChild(pathPart);
319                 currStack++;
320             }
321             else if(dirStack[currStack].data != pathPart) //If both they are the same, check for the next part
322             {
323                 dirStack = dirStack[0..$-1];
324                 currStack--;
325             }
326             else if(dirStack[currStack].data == pathPart) //If both they are the same, check for the next part
327                 currStack++;
328         }
329     }
330     return root;
331 }
332 string buildPath(Node!string node)
333 {
334     string ret = node.data;
335     while(node.parent !is null)
336     {
337         node = node.parent;
338         if(node)
339         {
340             ret = node.data~"/"~ret;
341         }
342     }
343     return ret;
344 }
345 
346 
347 ///Copied from dmd.
348 unittest
349 {
350     assert(baseName("a/b/test.txt") == "test.txt");
351     assert(relativePath("foo", "") == "foo");
352     assert(filenameNoExt("helloWorld.zip") == "helloWorld");
353     assert("/hello/test/again".isRootOf("/hello/test/again/something/is/here.txt"));
354 
355     version (Posix)
356     {
357         assert(filename("/something/here/yet.txt"), "yet.txt");
358         assert(filenameNoExt("/something/here/yet.txt"), "yet");
359 
360         assert(relativePath("foo", "/bar") == "foo");
361         assert(relativePath("/foo/bar", "/foo/bar") == ".");
362         assert(relativePath("/foo/bar", "/foo/baz") == "../bar");
363         assert(relativePath("/foo/bar/baz", "/foo/woo/wee") == "../../bar/baz");
364         assert(relativePath("/foo/bar/baz", "/foo/bar") == "baz");
365     }
366     version (Windows)
367     {
368         assert(filename(`c:\something\here\yet.txt`), "yet.txt");
369         assert(filenameNoExt(`c:\something\here\yet.txt`) == "yet");
370 
371         assert(relativePath("foo", `c:\bar`) == "foo");
372         assert(relativePath(`c:\foo\bar`, `c:\foo\bar`) == ".");
373         assert(relativePath(`c:\foo\bar`, `c:\foo\baz`) == `..\bar`);
374         assert(relativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`) == `..\..\bar\baz`);
375         assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz");
376         assert(relativePath(`c:\foo\bar`, `d:\foo`) == `c:\foo\bar`);
377     }
378 }